#!/usr/bin/python
# -*- coding: utf-8 -*-

import sys

sys.path.append("..")
import numpy as np
import scipy.special
import matplotlib.pyplot as plt

import time


class NeuralNetwork():
    # 初始化神经网络
    def __init__(self, inputnodes, hiddennodes, outputnodes, learningrate):
        # 设置输入层节点，隐藏层节点和输出层节点的数量和学习率
        self.inodes = inputnodes
        self.hnodes = hiddennodes
        self.onodes = outputnodes
        self.lr = learningrate  # 设置神经网络中的学习率
        # 使用正态分布，进行权重矩阵的初始化
        '''
        np.random.normal第一个参数【loc】： 分布的均值（中心/对称轴x坐标设置）
        np.random.normal第一个参数【scale】： 分布的标准差（宽度）
        np.random.normal第一个参数【size】： 输出值的维度。如果给定的维度为(m, n, k)，那么就从分布中抽取m * n * k个样本。如果size为None（默认值）并且loc和scale均为标量，那么就会返回一个值。否则会返回np.broadcast(loc, scale).size个值
        np.random.normal输出【out】： n维数组或标量（从含参的正态分布中抽取的随机样本）
        '''
        self.wih = np.random.normal(0.0, pow(self.hnodes, -0.5), (self.hnodes, self.inodes))
        self.who = np.random.normal(0.0, pow(self.onodes, -0.5), (self.onodes, self.hnodes))
        self.activation_function = lambda x: scipy.special.expit(x)  # 激活函数设为Sigmod()函数
        pass

    # 定义训练神经网络
    print("************Train start******************")

    def train(self, input_list, target_list):
        # # 转换输入/输出列表到二维数组，在外层多套(ndmin-1)层中括号，numpy dimension min，然后对矩阵进行转置
        inputs = np.array(input_list, ndmin=2).T
        targets = np.array(target_list, ndmin=2).T
        # （矩阵乘法）
        hidden_inputs = np.dot(self.wih, inputs)  # 计算到隐藏层的信号，dot()返回的是两个数组的点积
        hidden_outputs = self.activation_function(hidden_inputs)  # 计算隐藏层输出的信号
        final_inputs = np.dot(self.who, hidden_outputs)  # 计算到输出层的信号
        final_outputs = self.activation_function(final_inputs)

        output_errors = targets - final_outputs  # 计算输出值与标签值的差值
        # print("*****************************")
        # print("output_errors:",output_errors)
        hidden_errors = np.dot(self.who.T, output_errors)

        '''
        np.transpose函数
        使用 numpy.transpose ()进行变换，其实就是交换了坐标轴，如：x.transpose(1, 2, 0)，其实就是将x第二维度挪到第一维上，第三维移到第二维上，原本的第一维移动到第三维上
        举例：
        x = np.arange(12).reshape((2,3,2))
        x[0][0][0] = 0				x[1][0][0] = 6
        x[0][0][1] = 1				x[1][0][1] = 7
        x[0][1][0] = 2				x[1][1][0] = 8
        x[0][1][1] = 3				x[1][1][1] = 9
        x[0][2][0] = 4				x[1][2][0] = 10
        x[0][2][1] = 5				x[1][2][1] = 11
        转换后，shape = (3,2,2)
        x[0][0][0] = 0				x[0][0][1] = 6
        x[0][1][0] = 1				x[0][1][1] = 7
        x[1][0][0] = 2				x[1][0][1] = 8
        x[1][1][0] = 3				x[1][1][1] = 9
        x[2][0][0] = 4				x[2][0][1] = 10
        x[2][1][0] = 5				x[2][1][1] = 11
        '''
        # 隐藏层和输出层权重更新
        self.who += self.lr * np.dot((output_errors * final_outputs * (1.0 - final_outputs)),
                                     np.transpose(hidden_outputs))
        # 输入层和隐藏层权重更新
        self.wih += self.lr * np.dot((hidden_errors * hidden_outputs * (1.0 - hidden_outputs)),
                                     np.transpose(inputs))
        pass

        # 查询神经网络

    def query(self, input_list):  # 转换输入列表到二维数
        inputs = np.array(input_list, ndmin=2).T  # 计算到隐藏层的信号
        hidden_inputs = np.dot(self.wih, inputs)  # 计算隐藏层输出的信号
        hidden_outputs = self.activation_function(hidden_inputs)  # 计算到输出层的信号
        final_inputs = np.dot(self.who, hidden_outputs)  # numpy.dot() 矩阵乘法
        final_outputs = self.activation_function(final_inputs)
        return final_outputs


if __name__ == '__main__':
    """
        本次实验使用28 * 28 pixel大小的图片作为训练集和测试集，用于区分手写数字0~9。
        对于神经网络的搭建，需要：输入层、隐藏层和输出层。
        其中输入层张量宽度，可认为是一维，则有28*28=784个感知点。隐藏层数量随意。输出层设为10个。
    """
    # 定义神经网络训练模型，使用训练集对模型进行epochs次训练，不断调整权重
    input_nodes = 784  # 输入层神经元个数
    hidden_nodes = 100  # 隐藏层神经元个数
    output_nodes = 10  # 输出层神经元个数
    learning_rate = 0.3  # 学习率为0.3
    # 创建神经网络
    n = NeuralNetwork(input_nodes, hidden_nodes, output_nodes, learning_rate)

    # 读取训练数据集 转化为列表
    training_data_file = open("source/mnist_train.csv", 'r')
    training_data_list = training_data_file.readlines();  # 方法用于读取所有行，并返回列表
    # print("training_data_list:",training_data_list)
    training_data_file.close()

    train_start = time.perf_counter()  # 返回系统运行时间
    a = 0
    # 训练次数
    epochs = 5
    for e in range(epochs):
        # 训练神经网络
        for record in training_data_list:
            if a == 10000:  # 只训练其中一万张图
                break
            all_values = record.split(',')  # 根据逗号，将文本数据进行拆分
            if a == 1 or a == 2:  # 绘制数字
                image_arr = np.asfarray(all_values[1:]).reshape(28, 28)
                plt.imshow(image_arr, cmap='Greys', interpolation='None')
                plt.show()
            # 将文本字符串转化为实数，并创建这些数字的数组。
            inputs = (np.asfarray(all_values[1:]) / 255.0 * 0.99) + 0.01
            # 创建用零填充的数组，数组的长度为output_nodes,加0.01解决了0输入造成的问题
            targets = np.zeros(output_nodes) + 0.01
            # 使用目标标签，将正确元素设置为0.99
            targets[int(all_values[0])] = 0.99
            n.train(inputs, targets)
            pass
            a += 1
    pass
    train_end = time.perf_counter()
    print('train over, 用时：{:.4f}s'.format(train_end - train_start))

    # 模型测试，如果期望值和输出的值相同，就往score[]数组里面加1，否则加0，进而算准确率。
    test_data_file = open("source/mnist_test.csv", 'r')
    test_data_list = test_data_file.readlines()
    test_data_file.close()

    all_values = test_data_list[2].split(',')

    test_start = time.perf_counter()
    score = []
    print("***************Test start!**********************")
    for record in test_data_list:
        # 用逗号分割将数据进行拆分
        all_values = record.split(',')
        # 正确的答案是第一个值
        correct_values = int(all_values[0])
        # print(correct_values, "是正确的期望值")
        # 做输入
        inputs = (np.asfarray(all_values[1:]) / 255.0 * 0.99) + 0.01
        # 测试网络 作输入
        outputs = n.query(inputs)
        # 找出输出的最大值的索引
        label = np.argmax(outputs)
        # print(label, "是网络的输出值\n")
        # 如果期望值和网络的输出值正确 则往score 数组里面加1 否则添加0
        if (label == correct_values):
            score.append(1)
        else:
            score.append(0)
        pass
    pass
    test_end = time.perf_counter()
    print('test over, 用时：{:.4f}s'.format(test_end - test_start))

    # print(score)
    score_array = np.asfarray(score)
    print("正确率是：", (score_array.sum() / score_array.size) * 100, '%')

    # 每秒实时监控内存变化： $ watch -n 1 free -m
    # 输入jupyter notebook命令可以打开代理，浏览本地的ipynb文件
